1 module fswatch;
2 
3 
4 debug (FSWTestRun2) version = FSWForcePoll;
5 
6 ///
7 enum FileChangeEventType : ubyte
8 {
9 	/// Occurs when a file or folder is created.
10 	create,
11 	/// Occurs when a file or folder is modified.
12 	modify,
13 	/// Occurs when a file or folder is removed.
14 	remove,
15 	/// Occurs when a file or folder inside a folder is renamed.
16 	rename,
17 	/// Occurs when the watched path gets created.
18 	createSelf,
19 	/// Occurs when the watched path gets deleted.
20 	removeSelf
21 }
22 
23 /// Structure containing information about filesystem events.
24 struct FileChangeEvent
25 {
26 	/// The type of this event.
27 	FileChangeEventType type;
28 	/// The path of the file of this event. Might not be set for createSelf and removeSelf.
29 	string path;
30 	/// The path the file got renamed to for a rename event.
31 	string newPath = null;
32 }
33 
34 
35 
36 version (FSWForcePoll)
37 	version = FSWUsesPolling;
38 else
39 {
40 	version (Windows)
41 		version = FSWUsesWin32;
42 	else version (linux)
43 		version = FSWUsesINotify;
44 	else version = FSWUsesPolling;
45 }
46 
47 /// An instance of a FileWatcher
48 /// Contains different implementations (win32 api, inotify and polling using the std.file methods)
49 /// Specify `version = FSWForcePoll;` to force using std.file (is slower and more resource intensive than the other implementations)
50 struct FileWatch
51 {
52 	// internal path variable which shouldn't be changed because it will not update inotify/poll/win32 structures uniformly.
53 	string _path;
54 
55 	/// Path of the file set using the constructor
56 	const ref const(string) path() return @property @safe @nogc nothrow pure
57 	{
58 		return _path;
59 	}
60 
61 	version (FSWUsesWin32)
62 	{
63 
64 		/*
65 		 * The Windows version works by first creating an asynchronous path handle using CreateFile.
66 		 * The name may suggest this creates a new file on disk, but it actually gives
67 		 * a handle to basically anything I/O related. By using the flags FILE_FLAG_OVERLAPPED
68 		 * and FILE_FLAG_BACKUP_SEMANTICS it can be used in ReadDirectoryChangesW.
69 		 * 'Overlapped' here means asynchronous, it can also be done synchronously but that would
70 		 * mean getEvents() would wait until a directory change is registered.
71 		 * The asynchronous results can be received in a callback, but since FSWatch is polling
72 		 * based it polls the results using GetOverlappedResult. If messages are received,
73 		 * ReadDirectoryChangesW is called again.
74 		 * The function will not notify when the watched directory itself is removed, so
75 		 * if it doesn't exist anymore the handle is closed and set to null until it exists again.
76 		 */
77 		import core.sys.windows.basetsd : HANDLE;
78 		import fswatch_helpers;
79 
80 		import core.sys.windows.winbase: OPEN_EXISTING, FILE_FLAG_OVERLAPPED, OVERLAPPED,
81 			CloseHandle, GetOverlappedResult, CreateFile, GetLastError, ReadDirectoryChangesW,
82 			FILE_FLAG_BACKUP_SEMANTICS;
83 
84 		import core.sys.windows.winnt: FILE_NOTIFY_INFORMATION, FILE_ACTION_ADDED,
85 			FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED,
86 			FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME,
87 			FILE_LIST_DIRECTORY, FILE_SHARE_WRITE, FILE_SHARE_READ,
88 			FILE_SHARE_DELETE, FILE_NOTIFY_CHANGE_FILE_NAME,
89 			FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE,
90 			ERROR_IO_PENDING, ERROR_IO_INCOMPLETE, DWORD;
91 		import std.utf : toUTF8, toUTF16, toUTF16z;
92 		import hip.util.conv : to, toHex;
93 
94 		private HANDLE pathHandle; // Windows 'file' handle for ReadDirectoryChangesW
95 		private ubyte[1024 * 4] changeBuffer; // 4kb buffer for file changes
96 		private bool isDir, exists, recursive;
97 		private size_t timeLastModified;
98 		private DWORD receivedBytes;
99 		private OVERLAPPED overlapObj;
100 		private bool queued; // Whether a directory changes watch is issued to Windows
101 		private string _absolutePath;
102 
103 		/// Creates an instance using the Win32 API
104 		this(string path, bool recursive = false, bool treatDirAsFile = false)
105 		{
106 			import fswatch_helpers;
107 			import hip.util.path;
108 			_path = path;
109 			_absolutePath = joinPath(winGetCwd, path);
110 			this.recursive = recursive;
111 			isDir = !treatDirAsFile;
112 			if (!isDir && recursive)
113 				throw new Exception("Can't recursively check on a file");
114 			getEvents(); // To create a path handle and start the watch queue
115 			// The result, likely containing just 'createSelf' or 'removeSelf', is discarded
116 			// This way, the first actual call to getEvents() returns actual events
117 		}
118 
119 		~this()
120 		{
121 			CloseHandle(pathHandle);
122 		}
123 
124 		private void startWatchQueue()
125 		{
126 			if (!ReadDirectoryChangesW(pathHandle, changeBuffer.ptr, changeBuffer.length, recursive,
127 					FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
128 					&receivedBytes, &overlapObj, null))
129 				throw new Exception("Failed to start directory watch queue. Error 0x" ~ GetLastError()
130 					.toHex);
131 			queued = true;
132 		}
133 
134 		/// Implementation using Win32 API or polling for files
135 		FileChangeEvent[] getEvents()
136 		{
137 			const(wchar)* wAbsolutePath = toUTF16z(_absolutePath);
138 			const pathExists = winExists(wAbsolutePath); // cached so it is not called twice
139 			if (isDir && (!pathExists || winIsDir(wAbsolutePath)))
140 			{
141 				// ReadDirectoryChangesW does not report changes to the specified directory
142 				// itself, so 'removeself' is checked manually
143 				if (!pathExists)
144 				{
145 					if (pathHandle)
146 					{
147 						if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
148 						{
149 						}
150 						queued = false;
151 						CloseHandle(pathHandle);
152 						pathHandle = null;
153 						return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
154 					}
155 					return [];
156 				}
157 				FileChangeEvent[] events;
158 				if (!pathHandle)
159 				{
160 					pathHandle = CreateFile((_absolutePath.toUTF16 ~ cast(wchar) 0).ptr, FILE_LIST_DIRECTORY,
161 							FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
162 							null, OPEN_EXISTING,
163 							FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, null);
164 					if (!pathHandle)
165 						throw new Exception("Error opening directory. Error code 0x" ~ GetLastError()
166 								.toHex);
167 					queued = false;
168 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
169 				}
170 				if (!queued)
171 				{
172 					startWatchQueue();
173 				}
174 				else
175 				{
176 					// ReadDirectoryW can give double modify messages, making the queue one event behind
177 					// This sequence is repeated as a fix for now, until the intricacy of WinAPI is figured out
178 					foreach(_; 0..2)
179 					{
180 						if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
181 						{
182 							int i = 0;
183 							string fromFilename;
184 							while (true)
185 							{
186 								auto info = cast(FILE_NOTIFY_INFORMATION*)(changeBuffer.ptr + i);
187 								string fileName = (cast(wchar[])(
188 										cast(ubyte*) info.FileName)[0 .. info.FileNameLength])
189 									.toUTF8.idup;
190 								switch (info.Action)
191 								{
192 								case FILE_ACTION_ADDED:
193 									events ~= FileChangeEvent(FileChangeEventType.create, fileName);
194 									break;
195 								case FILE_ACTION_REMOVED:
196 									events ~= FileChangeEvent(FileChangeEventType.remove, fileName);
197 									break;
198 								case FILE_ACTION_MODIFIED:
199 									events ~= FileChangeEvent(FileChangeEventType.modify, fileName);
200 									break;
201 								case FILE_ACTION_RENAMED_OLD_NAME:
202 									fromFilename = fileName;
203 									break;
204 								case FILE_ACTION_RENAMED_NEW_NAME:
205 									events ~= FileChangeEvent(FileChangeEventType.rename,
206 											fromFilename, fileName);
207 									break;
208 								default:
209 									throw new Exception("Unknown file notify action 0x" ~ info.Action.toHex);
210 								}
211 								i += info.NextEntryOffset;
212 								if (info.NextEntryOffset == 0)
213 									break;
214 							}
215 							queued = false;
216 							startWatchQueue();
217 						}
218 						else
219 						{
220 							if (GetLastError() != ERROR_IO_PENDING
221 								&& GetLastError() != ERROR_IO_INCOMPLETE)
222 								throw new Exception("Error receiving changes. Error code 0x"~ GetLastError().toHex);
223 							break;
224 						}
225 					}
226 				}
227 				return events;
228 			}
229 			else
230 			{
231 				const nowExists = winExists(wAbsolutePath);
232 				if (nowExists && !exists)
233 				{
234 					exists = true;
235 					timeLastModified = fswatch_helpers.timeLastModified(wAbsolutePath);
236 					return [FileChangeEvent(FileChangeEventType.createSelf, _absolutePath)];
237 				}
238 				else if (!nowExists && exists)
239 				{
240 					exists = false;
241 					return [FileChangeEvent(FileChangeEventType.removeSelf, _absolutePath)];
242 				}
243 				else if (nowExists)
244 				{
245 					const modTime = fswatch_helpers.timeLastModified(wAbsolutePath);
246 					if (modTime != timeLastModified)
247 					{
248 						timeLastModified = modTime;
249 						return [FileChangeEvent(FileChangeEventType.modify, _absolutePath)];
250 					}
251 					else
252 						return [];
253 				}
254 				else
255 					return [];
256 			}
257 		}
258 	}
259 	else version (FSWUsesINotify)
260 	{
261 		
262 		import core.sys.linux.errno : errno;
263 		import core.sys.posix.poll : pollfd, poll, POLLIN;
264 
265 		import core.stdc.errno : ENOENT;
266 
267 		private int fd;
268 		private bool recursive;
269 		private ubyte[1024 * 4] eventBuffer; // 4kb buffer for events
270 		private pollfd pfd;
271 		private struct FDInfo { int wd; bool watched; string path; }
272 		private FDInfo[] directoryMap; // map every watch descriptor to a directory
273 
274 		/// Creates an instance using the linux inotify API
275 		this(string path, bool recursive = false, bool ignored = false)
276 		{
277 			_path = path;
278 			this.recursive = recursive;
279 			getEvents();
280 		}
281 
282 		~this()
283 		{
284 			import core.sys.linux.unistd : close;
285 			import core.sys.linux.sys.inotify: inotify_rm_watch;
286 			if (fd)
287 			{
288 				foreach (ref fdinfo; directoryMap)
289 					if (fdinfo.watched)
290 						inotify_rm_watch(fd, fdinfo.wd);
291 				close(fd);
292 			}
293 		}
294 
295 		private void addWatch(string path)
296 		{
297 			import hip.util.string : toStringz;
298 			import hip.util.conv : to;
299 			import core.sys.linux.fcntl : fcntl, F_SETFD, FD_CLOEXEC;
300 			import core.sys.linux.sys.inotify: inotify_add_watch, IN_CREATE, IN_DELETE,
301 				IN_DELETE_SELF, IN_MODIFY, IN_MOVE_SELF, IN_MOVED_FROM, IN_MOVED_TO,
302 				IN_ATTRIB, IN_EXCL_UNLINK;
303 			
304 
305 			auto wd = inotify_add_watch(fd, path.toStringz,
306 					IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF
307 					| IN_MOVED_FROM | IN_MOVED_TO | IN_ATTRIB | IN_EXCL_UNLINK);
308 			assert(wd != -1,
309 					"inotify_add_watch returned invalid watch descriptor. Error code "
310 					~ errno.to!string);
311 			assert(fcntl(fd, F_SETFD, FD_CLOEXEC) != -1,
312 					"Could not set FD_CLOEXEC bit. Error code " ~ errno.to!string);
313 			directoryMap ~= FDInfo(wd, true, path);
314 		}
315 
316 		/// Implementation using inotify
317 		FileChangeEvent[] getEvents()
318 		{
319 			import std.algorithm : countUntil;
320 			import std.file;
321 			import hip.util.string: toStringz;
322 			import hip.util.conv : to;
323 			import std.string : stripRight;
324 			import core.sys.linux.unistd : read;
325 			import core.sys.linux.fcntl : stat, stat_t, S_ISDIR;
326 			import core.sys.linux.sys.inotify : inotify_init1,
327 				inotify_event, IN_NONBLOCK, IN_MOVED_TO, IN_CREATE, IN_DELETE, IN_ATTRIB, IN_MOVED_FROM,
328 				IN_MOVE_SELF, IN_MODIFY, IN_DELETE_SELF, inotify_rm_watch;
329 
330 			FileChangeEvent[] events;
331 			if (!fd && path.exists)
332 			{
333 				fd = inotify_init1(IN_NONBLOCK);
334 				assert(fd != -1,
335 						"inotify_init1 returned invalid file descriptor. Error code "
336 						~ errno.to!string);
337 				addWatch(path);
338 				events ~= FileChangeEvent(FileChangeEventType.createSelf, path);
339 
340 				if (recursive)
341 					foreach(string subPath; dirEntries(path, SpanMode.depth))
342 					{
343 						addWatch(subPath);
344 						events ~= FileChangeEvent(FileChangeEventType.createSelf, subPath);
345 					}
346 			}
347 			if (!fd)
348 				return events;
349 			pfd.fd = fd;
350 			pfd.events = POLLIN;
351 			const code = poll(&pfd, 1, 0);
352 			if (code < 0)
353 				throw new Exception("Failed to poll events. Error code " ~ errno.to!string);
354 			else if (code == 0)
355 				return events;
356 			else
357 			{
358 				import hip.util.path:relativePath, joinPath;
359 				const receivedBytes = read(fd, eventBuffer.ptr, eventBuffer.length);
360 				int i = 0;
361 				string fromFilename;
362 				uint cookie;
363 				while (true)
364 				{
365 					auto info = cast(inotify_event*)(eventBuffer.ptr + i);
366 					// string fileName = info.name.ptr[0..info.len].idup;
367 					string fileName = info.name.ptr[0..info.len].stripRight("\0").idup;
368 					auto mapIndex = directoryMap.countUntil!(a => a.wd == info.wd);
369 					string absoluteFileName = joinPath('/', directoryMap[mapIndex].path, fileName);
370 					string relativeFilename = relativePath("/" ~ absoluteFileName, "/" ~ path);
371 					if (cookie && (info.mask & IN_MOVED_TO) == 0)
372 					{
373 						events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
374 						fromFilename.length = 0;
375 						cookie = 0;
376 					}
377 					if ((info.mask & IN_CREATE) != 0)
378 					{
379 						// If a dir/file is created and deleted immediately then
380 						// isDir will throw FileException(ENOENT)
381 						if (recursive)
382 						{
383 							stat_t dirCheck;
384 							if (stat(absoluteFileName.toStringz, &dirCheck) == 0)
385 							{
386 								if (S_ISDIR(dirCheck.st_mode))
387 									addWatch(absoluteFileName);
388 							}
389 							else
390 							{
391 								const err = errno;
392 								if (err != ENOENT)
393 									throw new FileException(absoluteFileName, err);
394 							}
395 						}
396 
397 						events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
398 					}
399 					if ((info.mask & IN_DELETE) != 0)
400 						events ~= FileChangeEvent(FileChangeEventType.remove, relativeFilename);
401 					if ((info.mask & IN_MODIFY) != 0 || (info.mask & IN_ATTRIB) != 0)
402 						events ~= FileChangeEvent(FileChangeEventType.modify, relativeFilename);
403 					if ((info.mask & IN_MOVED_FROM) != 0)
404 					{
405 						fromFilename = fileName;
406 						cookie = info.cookie;
407 					}
408 					if ((info.mask & IN_MOVED_TO) != 0)
409 					{
410 						if (info.cookie == cookie)
411 						{
412 							events ~= FileChangeEvent(FileChangeEventType.rename,
413 									fromFilename, relativeFilename);
414 						}
415 						else
416 							events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
417 						cookie = 0;
418 					}
419 					if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0)
420 					{
421 						if (fd)
422 						{
423 							inotify_rm_watch(fd, info.wd);
424 							directoryMap[mapIndex].watched = false;
425 						}
426 						if (directoryMap[mapIndex].path == path)
427 							events ~= FileChangeEvent(FileChangeEventType.removeSelf, ".");
428 					}
429 					i += inotify_event.sizeof + info.len;
430 					if (i >= receivedBytes)
431 						break;
432 				}
433 				if (cookie)
434 				{
435 					events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
436 					fromFilename.length = 0;
437 					cookie = 0;
438 				}
439 			}
440 			return events;
441 		}
442 	}
443 	else version (FSWUsesPolling)
444 	{
445 		import std.datetime : SysTime;
446 		import std.algorithm : countUntil, remove;
447 		import std.path : relativePath, absolutePath, baseName;
448 		private ulong getUniqueHash(DirEntry entry)
449 		{
450 			version (Windows)
451 				return entry.timeCreated.stdTime ^ cast(ulong) entry.attributes;
452 			else version (Posix)
453 				return entry.statBuf.st_ino | (cast(ulong) entry.statBuf.st_dev << 32UL);
454 			else
455 				return (entry.timeLastModified.stdTime ^ (
456 						cast(ulong) entry.attributes << 32UL) ^ entry.linkAttributes) * entry.size;
457 		}
458 
459 		private struct FileEntryCache
460 		{
461 			SysTime lastModification;
462 			const string name;
463 			bool isDirty;
464 			ulong uniqueHash;
465 		}
466 
467 		private FileEntryCache[] cache;
468 		private bool isDir, recursive, exists;
469 		private SysTime timeLastModified;
470 		private string cwd;
471 
472 		/// Generic fallback implementation using std.file.dirEntries
473 		this(string path, bool recursive = false, bool treatDirAsFile = false)
474 		{
475 			_path = path;
476 			cwd = getcwd;
477 			this.recursive = recursive;
478 			isDir = !treatDirAsFile;
479 			if (!isDir && recursive)
480 				throw new Exception("Can't recursively check on a file");
481 			getEvents();
482 		}
483 
484 		/// Generic polling implementation
485 		FileChangeEvent[] getEvents()
486 		{
487 			const nowExists = path.exists;
488 			if (isDir && (!nowExists || path.isDir))
489 			{
490 				FileChangeEvent[] events;
491 				if (nowExists && !exists)
492 				{
493 					exists = true;
494 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
495 				}
496 				if (!nowExists && exists)
497 				{
498 					exists = false;
499 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
500 				}
501 				if (!nowExists)
502 					return [];
503 				foreach (ref e; cache)
504 					e.isDirty = true;
505 				DirEntry[] created;
506 				foreach (file; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow))
507 				{
508 					auto newCache = FileEntryCache(file.timeLastModified,
509 							file.name, false, file.getUniqueHash);
510 					bool found = false;
511 					foreach (ref cacheEntry; cache)
512 					{
513 						if (cacheEntry.name == newCache.name)
514 						{
515 							if (cacheEntry.lastModification != newCache.lastModification)
516 							{
517 								cacheEntry.lastModification = newCache.lastModification;
518 								events ~= FileChangeEvent(FileChangeEventType.modify,
519 										relativePath(file.name.absolutePath(cwd),
520 											path.absolutePath(cwd)));
521 							}
522 							cacheEntry.isDirty = false;
523 							found = true;
524 							break;
525 						}
526 					}
527 					if (!found)
528 					{
529 						cache ~= newCache;
530 						created ~= file;
531 					}
532 				}
533 				foreach_reverse (i, ref e; cache)
534 				{
535 					if (e.isDirty)
536 					{
537 						auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e);
538 						if (idx != -1)
539 						{
540 							events ~= FileChangeEvent(FileChangeEventType.rename,
541 									relativePath(e.name.absolutePath(cwd),
542 										path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd),
543 										path.absolutePath(cwd)));
544 							created = created.remove(idx);
545 						}
546 						else
547 						{
548 							events ~= FileChangeEvent(FileChangeEventType.remove,
549 									relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
550 						}
551 						cache = cache.remove(i);
552 					}
553 				}
554 				foreach (ref e; created)
555 				{
556 					events ~= FileChangeEvent(FileChangeEventType.create,
557 							relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
558 				}
559 				if (events.length && events[0].type == FileChangeEventType.createSelf)
560 					return [events[0]];
561 				return events;
562 			}
563 			else
564 			{
565 				if (nowExists && !exists)
566 				{
567 					exists = true;
568 					timeLastModified = path.timeLastModified;
569 					return [FileChangeEvent(FileChangeEventType.createSelf, ".")];
570 				}
571 				else if (!nowExists && exists)
572 				{
573 					exists = false;
574 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
575 				}
576 				else if (nowExists)
577 				{
578 					const modTime = path.timeLastModified;
579 					if (modTime != timeLastModified)
580 					{
581 						timeLastModified = modTime;
582 						return [FileChangeEvent(FileChangeEventType.modify, path.baseName)];
583 					}
584 					else
585 						return [];
586 				}
587 				else
588 					return [];
589 			}
590 		}
591 	}
592 	else
593 		static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;");
594 }
595 
596 ///
597 unittest
598 {
599 	import core.thread;
600 
601 	FileChangeEvent waitForEvent(ref FileWatch watcher)
602 	{
603 		FileChangeEvent[] ret;
604 		while ((ret = watcher.getEvents()).length == 0)
605 		{
606 			Thread.sleep(1.msecs);
607 		}
608 		return ret[0];
609 	}
610 
611 	if (exists("test"))
612 		rmdirRecurse("test");
613 	scope (exit)
614 	{
615 		if (exists("test"))
616 			rmdirRecurse("test");
617 	}
618 
619 	auto watcher = FileWatch("test", true);
620 	assert(watcher.path == "test");
621 	mkdir("test");
622 	auto ev = waitForEvent(watcher);
623 	assert(ev.type == FileChangeEventType.createSelf);
624 	write("test/a.txt", "abc");
625 	ev = waitForEvent(watcher);
626 	assert(ev.type == FileChangeEventType.create);
627 	assert(ev.path == "a.txt");
628 	Thread.sleep(2000.msecs); // for polling variant
629 	append("test/a.txt", "def");
630 	ev = waitForEvent(watcher);
631 	assert(ev.type == FileChangeEventType.modify);
632 	assert(ev.path == "a.txt");
633 	rename("test/a.txt", "test/b.txt");
634 	ev = waitForEvent(watcher);
635 	assert(ev.type == FileChangeEventType.rename);
636 	assert(ev.path == "a.txt");
637 	assert(ev.newPath == "b.txt");
638 	remove("test/b.txt");
639 	ev = waitForEvent(watcher);
640 	assert(ev.type == FileChangeEventType.remove);
641 	assert(ev.path == "b.txt");
642 	rmdirRecurse("test");
643 	ev = waitForEvent(watcher);
644 	assert(ev.type == FileChangeEventType.removeSelf);
645 }
646 
647 version (linux) unittest
648 {
649 	import core.thread;
650 
651 	FileChangeEvent waitForEvent(ref FileWatch watcher, Duration timeout = 2.seconds)
652 	{
653 		FileChangeEvent[] ret;
654 		Duration elapsed;
655 		while ((ret = watcher.getEvents()).length == 0)
656 		{
657 			Thread.sleep(1.msecs);
658 			elapsed += 1.msecs;
659 			if (elapsed >= timeout)
660 				throw new Exception("timeout");
661 		}
662 		return ret[0];
663 	}
664 
665 	if (exists("test2"))
666 		rmdirRecurse("test2");
667 	if (exists("test3"))
668 		rmdirRecurse("test3");
669 	scope (exit)
670 	{
671 		if (exists("test2"))
672 			rmdirRecurse("test2");
673 		if (exists("test3"))
674 			rmdirRecurse("test3");
675 	}
676 
677 	auto watcher = FileWatch("test2", true);
678 	mkdir("test2");
679 	auto ev = waitForEvent(watcher);
680 	assert(ev.type == FileChangeEventType.createSelf);
681 	write("test2/a.txt", "abc");
682 	ev = waitForEvent(watcher);
683 	assert(ev.type == FileChangeEventType.create);
684 	assert(ev.path == "a.txt");
685 	rename("test2/a.txt", "./testfile-a.txt");
686 	ev = waitForEvent(watcher);
687 	assert(ev.type == FileChangeEventType.remove);
688 	assert(ev.path == "a.txt");
689 	rename("./testfile-a.txt", "test2/b.txt");
690 	ev = waitForEvent(watcher);
691 	assert(ev.type == FileChangeEventType.create);
692 	assert(ev.path == "b.txt");
693 	remove("test2/b.txt");
694 	ev = waitForEvent(watcher);
695 	assert(ev.type == FileChangeEventType.remove);
696 	assert(ev.path == "b.txt");
697 
698 	mkdir("test2/mydir");
699 	rmdir("test2/mydir");
700 	try
701 	{
702 		ev = waitForEvent(watcher);
703 		// waitForEvent only returns first event (just a test method anyway) because on windows or unprecise platforms events can be spawned multiple times
704 		// or could be never fired in case of slow polling mechanism
705 		assert(ev.type == FileChangeEventType.create);
706 		assert(ev.path == "mydir");
707 	}
708 	catch (Exception e)
709 	{
710 		if (e.msg != "timeout")
711 			throw e;
712 	}
713 
714 	version (FSWUsesINotify)
715 	{
716 		// test for creation, modification, removal of subdirectory
717 		mkdir("test2/subdir");
718 		ev = waitForEvent(watcher);
719 		assert(ev.type == FileChangeEventType.create);
720 		assert(ev.path == "subdir");
721 		write("test2/subdir/c.txt", "abc");
722 		ev = waitForEvent(watcher);
723 		assert(ev.type == FileChangeEventType.create);
724 		assert(ev.path == "subdir/c.txt");
725 		write("test2/subdir/c.txt", "\nabc");
726 		ev = waitForEvent(watcher);
727 		assert(ev.type == FileChangeEventType.modify);
728 		assert(ev.path == "subdir/c.txt");
729 		rmdirRecurse("test2/subdir");
730 		auto events = watcher.getEvents();
731 		assert(events[0].type == FileChangeEventType.remove);
732 		assert(events[0].path == "subdir/c.txt");
733 		assert(events[1].type == FileChangeEventType.remove);
734 		assert(events[1].path == "subdir");
735 	}
736 	// removal of watched folder
737 	rmdirRecurse("test2");
738 	ev = waitForEvent(watcher);
739 	assert(ev.type == FileChangeEventType.removeSelf);
740 	assert(ev.path == ".");
741 
742 	version (FSWUsesINotify)
743 	{
744 		// test for a subdirectory already present
745 		// both when recursive = true and recursive = false
746 		foreach (recursive; [true, false])
747 		{
748 			mkdir("test3");
749 			mkdir("test3/a");
750 			mkdir("test3/a/b");
751 			watcher = FileWatch("test3", recursive);
752 			write("test3/a/b/c.txt", "abc");
753 			if (recursive)
754 			{
755 				ev = waitForEvent(watcher);
756 				assert(ev.type == FileChangeEventType.create);
757 				assert(ev.path == "a/b/c.txt");
758 			}
759 			if (!recursive)
760 			{
761 				// creation of subdirectory and file within
762 				// test that addWatch doesn't get called
763 				mkdir("test3/d");
764 				write("test3/d/e.txt", "abc");
765 				auto revents = watcher.getEvents();
766 				assert(revents.length == 1);
767 				assert(revents[0].type == FileChangeEventType.create);
768 				assert(revents[0].path == "d");
769 				rmdirRecurse("test3/d");
770 				revents = watcher.getEvents();
771 				assert(revents.length == 1);
772 				assert(revents[0].type == FileChangeEventType.remove);
773 				assert(revents[0].path == "d");
774 			}
775 			rmdirRecurse("test3");
776 			events = watcher.getEvents();
777 			if (recursive)
778 			{
779 				assert(events.length == 4);
780 				assert(events[0].type == FileChangeEventType.remove);
781 				assert(events[0].path == "a/b/c.txt");
782 				assert(events[1].type == FileChangeEventType.remove);
783 				assert(events[1].path == "a/b");
784 				assert(events[2].type == FileChangeEventType.remove);
785 				assert(events[2].path == "a");
786 				assert(events[3].type == FileChangeEventType.removeSelf);
787 				assert(events[3].path == ".");
788 			}
789 			else
790 			{
791 				assert(events.length == 2);
792 				assert(events[0].type == FileChangeEventType.remove);
793 				assert(events[0].path == "a");
794 				assert(events[1].type == FileChangeEventType.removeSelf);
795 				assert(events[1].path == ".");
796 			}
797 		}
798 	}
799 }